﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SetListManager.Models;
using System.Data.Objects;
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.IO;
using SetListManager.Helpers;

namespace SetListManager.Controllers
{

    [HandleError]
    [BandAuthorize]
    public class SetListController : Controller
    {
        //
        // Our Database context object
        // 
        private SLMDbContext db = new SLMDbContext();

        // Retrieves set lists asynchronously via AJAX
        public JsonResult RetrieveSetLists(jQueryDataTableParamModel param)
        {
            // We need to do our query in 2 steps.  The first step will pull from the SQL
            // server, the second will format our data.  The reason for this is that our
            // formatting operations cannot be run on the SQL server, so we can't
            // include them in our query that will run on SQL.

            // Query for all the set lists - goes to SQL
            var rows = from s in db.SetLists
//                       where s.BandID == CurrentBand.ID
                       select s;

            // Now we need to iterate each set list, formatting information correctly
            // for our JSON.  This one does not go to SQL.

//                new DateTime(Model.Ticks).ToString("h:mm tt")

            var result = from sl in rows.ToList()
                         select new
                         {
                             sl.Name,
                             GigDateTimeFormatted = sl.GigDateTimeFormatted,//String.Format("{0:d} {1:d}", sl.GigDate, sl.GigTime),
                             SetDuration = sl.SetDuration.FormatHrsMin,
                             sl.SetListID
                         };

            // Return our set list data in JSON, formatted specifically
            // for our jQueryDataTable object
            return Json(new
            {
                sEcho = param.sEcho,
                iTotalRecords = result.Count(),
                iTotalDisplayRecords = result.Count(),
                aaData = result
            },
            JsonRequestBehavior.AllowGet);
        }

        //
        // GET: /SetList/

        public ActionResult Index()
        {

            var query = from s in db.SetLists
                        select s;

            return View(query);
        }

        //
        // GET: /SetList/Details/5

        public ActionResult Details(int id)
        {
            SetListSongsModel sls = new SetListSongsModel();

            // Query for the specified set list
            sls.SetList = (from sl in db.SetLists
                               where sl.SetListID == id
                               select sl).First();

            // Query for the songs in the specified set list.  Explicitly include our
            // navigation property "Song" so we can get to details of the song, such as
            // song name or song duration.
            sls.SetListItems = from s in db.SetListItems
                               .Include("Song")
                               where s.SetListID == id
                               orderby s.SetNumber, s.SongOrder
                               select s;

            return View(sls);
        }



        //
        // GET: /SetList/Create

        public ActionResult Create()
        {
            SetList sl = new SetList();
            sl.GigDate = System.DateTime.Now;
            sl.GigTime = new TimeSpan(15, 0, 0);
            return View(sl);
        } 

        //
        // POST: /SetList/Create

        /// <summary>
        /// POST: /SetList/Create
        /// 
        /// Creates a new setlist using the specified posted data.
        /// Uses custom model binder to ensure TimeSpan is bound correctly.
        /// </summary>
        /// <param name="newset"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult Create([ModelBinder(typeof(MyTimeSpanModelBinder))][Bind(Exclude = "SetListID")] SetList newset)
        {
            // To create a new set list, we need a SetListID to pass to the "Edit" screen, 
            // but since we're creating a new set list, we don't have a SetListID yet.  
            // So...
            // The way to fix this is to have this method create a new blank set list 
            // and save it to the database, at which point it will get assigned a new
            // SetListID.  Then we need to query the DB again to find the newly-saved
            // set list, retrieve it's ID, and then invoke our "Edit" screen.
            try
            {
                // Save to DB.  Since we want to immediately go into EDIT mode, we'll
                // need to do some gyrations to retrieve our SetListID.
                
                // Generate random number so we can find our record
                Random _r = new Random();
                int randomID = _r.Next();
                newset.BandID = randomID;
                newset.SetDuration = new MinSec(0, 0);

                // Add to DB
                db.SetLists.Add(newset);
                db.SaveChanges();

                // Now retrieve the newly saved record to get it's ID
                var newrec = (from s in db.SetLists
                              where s.BandID == randomID
                              select s).First();

                // Update the BandID field to be correct
                newrec.BandID = CurrentBand.ID;
                db.SaveChanges(); // Save BandID

                // Redirect to EDIT
                return RedirectToAction("Edit/" + newrec.SetListID);
            }
            catch
            {
                return View();
            }
        }
        
        //
        // GET: /SetList/Edit/5
 
        public ActionResult Edit(int id)
        {

            SetListSongsModel sls = new SetListSongsModel();

            // Query for the specified set list
            var sl = (from set in db.SetLists
                           where set.SetListID == id
                           select set).FirstOrDefault();

            if (sl != null)
            {
                sls.SetList = sl;
            }
            else 
                throw new ApplicationException("Error 1"); //??


            // Query for the songs in the specified set list.  Explicitly include our
            // navigation property "Song" so we can get to details of the song, such as
            // song name or song duration.
            sls.SetListItems = from s in db.SetListItems
                               .Include("Song")
                               where s.SetListID == id
                               orderby s.SetNumber, s.SongOrder
                               select s;
            return View(sls);
        }


        public class SongListFilterOptions
        {
            public string SearchString { get; set; }
        }

        /// <summary>
        /// Helper for our Edit action: loads the song list using the specified filtering
        /// options. 
        /// </summary>
        /// <returns></returns>
        public ActionResult LoadSongList(SongListFilterOptions options)
        {
            // Grab all songs for this band, sorted by name.  If search string 
            // is specified, use it in the search
            var songs = from s in db.Songs
                        where (s.BandID == CurrentBand.ID &&
                            (s.Title.StartsWith(options.SearchString) || options.SearchString == null))
                        orderby s.Title
                        select s;
            return PartialView("SongListPartial", songs);
        }

        /// <summary>
        /// Helper for our Edit action: loads the set list and renders a partial view
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public ActionResult LoadSetList(int id)
        {
            // Grab set list items, sorted by set number and song order
            var SetListItems = from s in db.SetListItems
                               .Include("Song")
                               where s.SetListID == id
                               orderby s.SetNumber, s.SongOrder
                               select s;

            return PartialView("SetListPartial", SetListItems);
        }

        /// <summary>
        /// Our model for our SaveSetList method.  This model is serialized as a JSON
        /// string by javascript code, so it must match the model in the javascript.
        /// </summary>
        [Serializable]
        public class SaveSetListModel
        {
            public string Name { get; set; }
            public string Date { get; set; }  // Bring in as a string, we'll convert to DateTime later
            public string Time { get; set; }  // Bring in as a string, we'll convert to TimeSpan later
            public int SetListID { get; set; }

            /// <summary>
            /// Set Array -- contains an array of arrays.  Each array
            /// represents one set, items in that array are set list items.  Notes
            /// begin with 'Note:' and regular song items contain the songID.
            /// </summary>
            public string[][] SetArray { get; set; }
        }

        /// <summary>
        /// Our return result from our SaveSetList
        /// </summary>
        [Serializable]
        public class JsonReturnModel
        {
            public string Message { get; set; }
        }


        /// <summary>
        /// /SetList/SaveSetList
        /// This is an async call which returns a JSON object.
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult SaveSetList(SaveSetListModel result)
        {
            // Get current set list object
            SetList setlist = (from s in db.SetLists
                               where s.SetListID == result.SetListID
                               select s).First();

            // Delete our old SetListItem records (but let's reuse the SetList record)
            var sli = from s in db.SetListItems
                      where s.SetListID == result.SetListID
                      select s;
            foreach (var item in sli)
            {
                db.SetListItems.Remove(item);
            }

            // ************************************************************************************
            // BEGIN SAVE: iterate each set list item and store as a new SetListItem in DB
            // ************************************************************************************

            var set_number = 0; // used to iterate and track set number
            int set_list_length = 0; // total set list length in seconds

            if (result.SetArray != null && result.SetArray.Count() > 0)
            {

                // Create our new SetListItem records
                foreach (var setlistarray in result.SetArray)
                {
                    // Iterate each set
                    set_number++;
                    int song_index = 0;
                    foreach (var item in setlistarray)
                    {
                        // Iterate each setlistitem in this set
                        song_index++;
                        SetListItem newitem;

                        // If a note, then create a note item, else create a Song item
                        if (item.StartsWith("Note", true, System.Globalization.CultureInfo.CurrentCulture))
                        {
                            newitem = new SetListItem
                            {
                                SetListID = result.SetListID,
                                SetNumber = set_number,
                                SongOrder = song_index,
                                Note = item.Substring(6) // Remove the "Note:" part
                            };
                        }
                        else
                        {
                            // It's a song item.  Create a song item.
                            newitem = new SetListItem
                            {
                                SetListID = result.SetListID,
                                SetNumber = set_number,
                                SongOrder = song_index,
                                // Create song.  

                                // BUGBUG The Find() statement below calls SQL to retrieve all the
                                // song fields.  Not sure if there is a better way to do this to
                                // reduce SQL roundtrips.
                                Song = db.Songs.Find(Convert.ToInt16(item))
                            };

                            // Calculate set list length
                            set_list_length += Convert.ToInt16(newitem.Song.Duration.FormatSeconds);
                            set_list_length += 60; // PAUSE HACK: add 60 seconds to account for pauses between songs.
                            // TODO: WE NEED TO MAKE THE ABOVE HACK A SETTABLE OPTION.
                        }
                        db.SetListItems.Add(newitem);
                    }
                }
            }


            // Update the rest of our SetList model data
            setlist.Name = result.Name;
            setlist.SetDuration = new MinSec(0, set_list_length);

            // Parse date into DateTime format for DB
            setlist.GigDate = System.DateTime.Parse(result.Date);

            // Parse TimeSpan.  We already have a good TimeSpan parser 
            // in our custom ModelBinder, so use that one
            TimeSpan ts;
            MyTimeSpanModelBinder.TryParseTime(result.Time, out ts);
            setlist.GigTime = ts;

            // Save changes
            if (ModelState.IsValid)
            {
                db.SaveChanges();
                string message = "Success";
                return Json(new JsonReturnModel { Message = message });
                //                    return RedirectToAction("Index");
            }

            else
            {
                string errorMessage = "<div class=\"validation-summary-errors\">"
                  + "The following errors occurred:<ul>";
                foreach (var key in ModelState.Keys)
                {
                    var error = ModelState[key].Errors.FirstOrDefault();
                    if (error != null)
                    {
                        errorMessage += "<li class=\"field-validation-error\">"
                         + error.ErrorMessage + "</li>";
                    }
                }
                errorMessage += "</ul>";
                return Json(new JsonReturnModel { Message = errorMessage });
            }

        }
        //
        // POST: /SetList/Edit/5

        [HttpPost]
        public ActionResult Edit(int id, FormCollection collection)
        {
            try
            {
                // TODO: Add update logic here
 
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        //
        // GET: /SetList/Delete/5
        [HttpGet]
        public ActionResult Delete(int id)
        {
            // Query for the specified set list
            var result = (from sl in db.SetLists
                               where sl.SetListID == id
                               select sl).First();
            return View(result);
        }

        //
        // POST: /SetList/Delete/5
        // Use anti-forgery token for XSRF
        // [ValidateAntiForgeryToken]
        [HttpPost]
        public ActionResult Delete(int id, FormCollection form)
        {
            // Remove set list record from table
            var setlist = db.SetLists.Find(id);
            db.SetLists.Remove(setlist);

            // Also remove all relevant SetListItems
            var setlistitems = from sli in db.SetListItems
                               where sli.SetListID == id
                               select sli;
            foreach (SetListItem sli in setlistitems)
            {
                db.SetListItems.Remove(sli);
            }

            db.SaveChanges();
            return RedirectToAction("Index");
        }



        //
        // GET: /SetList/CopyTo/5
        // Returns view to ask user what the name/date/time of the new set list should be
        [HttpGet]
        public ActionResult CopyTo(int id)
        {
            // Query for the specified set list
            var result = (from sl in db.SetLists
                          where sl.SetListID == id
                          select sl).First();

            // Clear out a few fields to not confuse the user
            result.GigDate = DateTime.Now;
            result.GigTime = TimeSpan.Zero;
            return View(result);
        }

        /// <summary>
        /// Update specified set list's duration field
        /// </summary>
        /// <param name="id"></param>
        public bool UpdateSetListDuration(int id)
        {
            // Find specified set list.  Include items.
            var setlist = (from sl in db.SetLists
                          .Include("Items")
                          .Include("Items.Song")
                          where sl.SetListID == id
                          select sl).First();

            int duration_seconds = 0;
            // Iterate set list items, add up timings
            foreach (SetListItem sli in setlist.Items)
            {
                duration_seconds += Convert.ToInt16(sli.Song.Duration.FormatSeconds);
                duration_seconds += 60; // PAUSE HACK: add 60 seconds to account for pauses between songs.
                // TODO: WE NEED TO MAKE THE ABOVE HACK A SETTABLE OPTION.
            }

            setlist.SetDuration = new MinSec(0, duration_seconds);
            db.SaveChanges();
            return true;
        }


        //
        // POST: /SetList/CopyTo/5
        // Copies specified set list to a new (newly-created) set list. 
        [HttpPost]
        public ActionResult CopyTo(int id, FormCollection form)
        {
            // Create new set list record. 
            SetList newset = new SetList();

            // Fill up data items from form
            bool bReturn = TryUpdateModel(newset,
                new string[] { "GigDate", "GigTime", "Name" },
                form.ToValueProvider());

            // Save our new set list to the DB.  We'll need to retrieve it so we can
            // associate the set list items with it, so use our technique of
            // generating a random number then searching for it.

            // Generate random number so we can find our record
            Random _r = new Random();
            int randomID = _r.Next();
            newset.BandID = randomID;
            newset.SetDuration = new MinSec(0, 0);

            // Add to DB
            db.SetLists.Add(newset);
            db.SaveChanges();

            // Now retrieve the newly saved record to get it's ID
            var newrec = (from s in db.SetLists
                          where s.BandID == randomID
                          select s).First();

            // Update the BandID field to be correct
            newrec.BandID = CurrentBand.ID;

            // We now have a new SetList representing our new set.  Now we need to copy 
            // the old set list items to new one.
            var oldsetlistitems = from oldsli in db.SetListItems
                                  .Include("Song")
                                  where oldsli.SetListID == id
                                  select oldsli;

            // Iterate set list items, copy each
            SetListItem newsli;
            foreach (SetListItem oldsli in oldsetlistitems)
            {
                // Create new set list item, copy fields
                newsli = new SetListItem
                {
                    SetListID = newrec.SetListID, // associated with new set list
                    SetNumber = oldsli.SetNumber, // copy old info
                    SongOrder = oldsli.SongOrder,

                    // BUGBUG The Find() statement below calls SQL to retrieve all the
                    // song fields.  Not sure if there is a better way to do this to
                    // reduce SQL roundtrips.
                    Song = db.Songs.Find(oldsli.Song.SongID)
                };
                db.SetListItems.Add(newsli); // Add to DB
            }

            // Save, done.
            db.SaveChanges();

            // Update set list duration field
            UpdateSetListDuration(newrec.SetListID);
            return RedirectToAction("Index");
        }

        /// <summary>
        /// GET:
        /// /Setlist/Print/5
        /// 
        /// Brings up the print options dialog for setlist 5
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public ActionResult Print(int id)
        {
            SetListPrintingOptions slpo = new SetListPrintingOptions();

            //slpo.SetList = new SetList();
            //slpo.Band = new Band();

            // Query for the specified set list, include set list songs (e.g. items)
            slpo.SetList = (from set in db.SetLists
                  where set.SetListID == id
                  select set).First();

            // Get band info
            slpo.Band = (from band in db.Bands
                 where band.BandID == CurrentBand.ID
                 select band).First();

            // Set up options for font selections
            // TODO: This needs to be at a more global scope somehow
            slpo.lFontSizes = new List<SelectListItem>();
            slpo.lFontSizes.Add(new SelectListItem
            {
                Text = "Small",
                Value = "1"
            });
            slpo.lFontSizes.Add(new SelectListItem
            {
                Text = "Medium",
                Value = "2"
            });
            slpo.lFontSizes.Add(new SelectListItem
            {
                Text = "Large",
                Value = "3"
            });

            // Defaults
            // TODO: pull from band and/or user settings
            slpo.iFontSizeColumns = 2;
            slpo.iFontSizeSong = 3;
            slpo.iFontSizeNotes = 1;

            // Set other defaults to populate screen
            // TODO: Pull from band and/or user settings 
            slpo.bPrintKey = true;
            slpo.bPrintNote1 = true;
            slpo.bPrintNote2 = false;
            slpo.bPrintNote3 = false;
            slpo.bPrintNote4 = false;
            slpo.bPrintNote5 = false;
            slpo.bPrintSoloist = true;
            slpo.bPrintTempo = false;
            slpo.bTruncateSongNames = true;

            return View(slpo);

        }

        /// <summary>
        /// POST:
        /// /Setlist/OutputPDF
        /// 
        /// Streams a PDF of the specified setlist to the output stream, which will open
        /// the PDF in the browser.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [AcceptVerbs(HttpVerbs.Post)]
        public FileStreamResult Print(int id, FormCollection collection)
        {
            SetListPrintingOptions slp = new SetListPrintingOptions();

            // Copy values from form collection
            // TODO: for safety we should use whitelist and also validate inputs
            bool ret = TryUpdateModel(slp, collection.ToValueProvider());

            // At this point, we have a partial SetListPrintingOptions() object, filled in
            // with values from our printing options page.  We now need to
            // fill in the rest of the data so we can send it to our PDF generator function.

            // Query for the specified set list
            slp.SetList = (from sl in db.SetLists
                           where sl.SetListID == id
                           select sl).First();

            // Query for the songs in the specified set list.  Explicitly include our
            // navigation property "Song" so we can get to details of the song, such as
            // song name or song duration.
            slp.SetListItems = from s in db.SetListItems
                               .Include("Song")
                               where s.SetListID == id
                               orderby s.SetNumber, s.SongOrder
                               select s;

            // Query for band info
            slp.Band = (from b in db.Bands
                       where b.BandID == CurrentBand.ID
                       select b).First();

            // Stream the PDF of the set list
            return StreamPDFSetList(slp);
        }

        /// <summary>
        /// TruncateTextToBox()
        /// 
        /// Helper function which returns a Chunk of text, truncated to fit
        /// in the specified width, given the specified font.
        /// </summary>
        /// <param name="text"></param>
        /// <param name="width"></param>
        /// <param name="font"></param>
        /// <returns></returns>
        public Chunk TruncateTextToBox(
                String text, // the text you'd like to fit, best case
                int width, // the width it needs to fit in, using PDF page units
                Font font // the font you're using
            )
        {
            Chunk aChunk = new Chunk(text, font);
            if (aChunk.GetWidthPoint() <= width )
                return aChunk;
            bool pass = false;

            // Iterate, lopping off letters till it fits
            for (int i = width ; i >= 0 && !pass; --i)
            {
                // Remove one char from string
                text = text.Substring(0, text.Length - 1);
                aChunk = new Chunk(text, font);
                pass = aChunk.GetWidthPoint() <= width;
            }
            return aChunk;
        }

        /// <summary>
        /// AddHeaderCell
        ///
        /// Helper function to add a specially-formatted header cell to table.
        /// </summary>
        /// <param name="text"></param>
        /// <param name="table"></param>
        public void AddHeaderCell(string text, ref PdfPTable table)
        {
            PdfPCell hd = new PdfPCell(new Phrase(text, fontTableHeader));
            hd.HorizontalAlignment = Element.ALIGN_LEFT;
            hd.BorderColor = BaseColor.WHITE;
            hd.PaddingBottom = 4;
            table.AddCell(hd);
        }

        /// <summary>
        /// AddNoteCell
        /// 
        /// Helper function to add a full-width note to the set list
        /// </summary>
        /// <param name="text"></param>
        /// <param name="table"></param>
        /// <param name="colcount"></param>
        public void AddNoteCell(string text, ref PdfPTable table, int colcount, Font font)
        {
            PdfPCell hd = new PdfPCell(new Phrase(text, font));
            hd.HorizontalAlignment = Element.ALIGN_LEFT;
            hd.Colspan = colcount;
            hd.Border = 0;
            table.AddCell(hd);
        }

        // Global fonts
        static BaseFont timesNormal = BaseFont.CreateFont(BaseFont.TIMES_ROMAN,
                                                    BaseFont.CP1252,
                                                    BaseFont.NOT_EMBEDDED);
        static BaseFont arialNormal = BaseFont.CreateFont(BaseFont.HELVETICA,
                                                    BaseFont.CP1252,
                                                    BaseFont.NOT_EMBEDDED);
        Font fontNormal = new Font(arialNormal, 10, Font.NORMAL);
        Font fontH1 = new Font(arialNormal, 16, Font.NORMAL);
        Font fontH2 = new Font(timesNormal, 12, Font.NORMAL);
        Font fontTableHeader = new Font(arialNormal, 14, Font.NORMAL);
        Font fontBandName = new Font(arialNormal, 26, Font.NORMAL);
        Font fontSetNumber = new Font(arialNormal, 26, Font.BOLD);

        Paragraph NewLine = new Paragraph("\n");
        Paragraph NewLineSmall = new Paragraph("\n", new Font(arialNormal, 4));

        /// <summary>
        /// Calculates set list length based on song durations
        /// </summary>
        /// <param name="items"></param>
        /// <returns></returns>
        public MinSec CalculateSetListLength(SetListItem[] items)
        {
            int seconds = 0;
            foreach (SetListItem sli in items)
            {
                seconds += Convert.ToInt16(sli.Song.Duration.FormatSeconds);
                seconds += 60; // PAUSE HACK: add 60 seconds to account for pauses between songs.
                // TODO: WE NEED TO MAKE THE ABOVE HACK A SETTABLE OPTION.
            }
            return new MinSec(0, seconds);
        }


        /// <summary>
        /// StreamPDFSetList
        /// 
        /// Streams specified set list to the output stream, resulting in a PDF
        /// getting sent to your browser.  The browser should open up Adobe Reader
        /// and display it properly.
        /// </summary>
        /// <param name="slp"></param>
        /// <returns></returns>
        public FileStreamResult StreamPDFSetList(SetListPrintingOptions slp)
        {
            MemoryStream ms = new MemoryStream();

            // Create iTextSharp document
            Document document = new Document(PageSize.LETTER);

            // iTextSharp PDF writer
            PdfWriter writer = PdfWriter.GetInstance(document, ms);

            // We will be printing one set per page.  
            // Get the number of sets so we can plan accordingly.
            int count_sets = 0;
            foreach (SetListItem sli in slp.SetList.Items)
            {
                if (sli.SetNumber > count_sets)
                {
                    count_sets = sli.SetNumber;
                }
            }

            // Open the iTextSharp document
            document.Open();
            
            // Set up fonts for this document
            int fsSong= 32;
            switch (slp.iFontSizeSong)
            {
                case 1:
                    fsSong = 18;
                    break;
                case 2:
                    fsSong = 24;
                    break;
                case 3:
                default:
                    fsSong = 32;
                    break;
            }

            int fsCol = 18;
            switch (slp.iFontSizeColumns)
            {
                case 1:
                    fsCol = 10;
                    break;
                case 2:
                    fsCol = 16;
                    break;
                case 3:
                default:
                    fsCol = 22;
                    break;
            }


            int fsNotes = 18;
            switch (slp.iFontSizeNotes)
            {
                case 1:
                    fsNotes = 16;
                    break;
                case 2:
                    fsNotes = 20;
                    break;
                case 3:
                default:
                    fsNotes = 24;
                    break;
            }

            Font fontSongName = new Font(arialNormal, fsSong, Font.NORMAL);
            Font fontColumns = new Font(arialNormal, fsCol, Font.NORMAL);
            Font fontNotes = new Font(arialNormal, fsNotes, Font.NORMAL);

            // BIG Loop.  One page per set.
            for (int current_setnumber = 1; current_setnumber <= count_sets; current_setnumber++)
            {
                // *******************************
                // Begin page
                // *******************************

                // TODO: add header image?
                // Image headerlogo = Image.GetInstance("C:\\Temp\\citylogo.png");
                // document.Add(headerlogo);

                // *******************************
                // Header table - 2 columns, band name and date on left, set number on right
                // *******************************
                PdfPTable table_hdr = new PdfPTable(2);
                int[] widths_hdr = { 75, 25 };
                table_hdr.SetWidths(widths_hdr);

                table_hdr.DefaultCell.Padding = 2;
                table_hdr.WidthPercentage = 100;  // percentage
                table_hdr.TotalWidth = document.Right - document.Left;
                table_hdr.DefaultCell.BorderWidth = 0;
                table_hdr.DefaultCell.HorizontalAlignment = Element.ALIGN_LEFT;
                table_hdr.DefaultCell.VerticalAlignment = Element.ALIGN_TOP;

                // Left column: band name and set list name/date
                Paragraph para = new Paragraph();
                para.Add(new Phrase(CurrentBand.Name, fontBandName));
                para.Add(new Phrase("\n", new Font(arialNormal, 2)));
                para.Add(new Phrase("\n", new Font(arialNormal, 4)));
                para.Add(new Phrase(slp.SetList.Name + " - " + slp.SetList.GigDateTimeFormatted, fontH2));
                table_hdr.AddCell(para);

                // Right column: set list number and set length
                Paragraph para2 = new Paragraph();
                para2.Add(new Phrase("Set " + current_setnumber, fontSetNumber));
                para2.Add(new Phrase("\n", new Font(arialNormal, 2)));
                para2.Add(new Phrase("\n", new Font(arialNormal, 4)));
                // Determine set length: select set list items for this set only
                var thissetitems = from sli in slp.SetListItems
                                   where sli.SetNumber == current_setnumber
                                   select sli;
                MinSec set_dur = CalculateSetListLength(thissetitems.ToArray());
                para2.Add(new Phrase("Length: " + set_dur.FormatMinSec, fontH2));

                PdfPCell cell = new PdfPCell(para2);
                cell.HorizontalAlignment = Element.ALIGN_RIGHT;
                cell.BorderWidth = 0;
                table_hdr.AddCell(cell);

                // Add header table to document
                document.Add(table_hdr);
                document.Add(NewLineSmall);

                // Horizontal Line
                PdfContentByte cb = writer.DirectContent;
                float curY = writer.GetVerticalPosition(true);
                cb.MoveTo(document.LeftMargin, curY);
                cb.LineTo(document.PageSize.Width - document.RightMargin, curY);
                cb.Stroke();
                document.Add(NewLineSmall);

                // *******************************
                // Add the songs in the set list.  
                // *******************************

                // How many columns do we need?
                int iColumnCount = 1 + // for "Song Name" - we always print that
                    (slp.bPrintSoloist ? 1 : 0) +
                    (slp.bPrintKey ? 1 : 0) +
                    (slp.bPrintTempo ? 1 : 0);

                // Count custom columns to print.  
                iColumnCount +=
                    ((slp.bPrintNote1 && slp.Band.Note1IsColumn) ? 1 : 0) +
                    ((slp.bPrintNote2 && slp.Band.Note2IsColumn) ? 1 : 0) +
                    ((slp.bPrintNote3 && slp.Band.Note3IsColumn) ? 1 : 0) +
                    ((slp.bPrintNote4 && slp.Band.Note4IsColumn) ? 1 : 0) +
                    ((slp.bPrintNote5 && slp.Band.Note5IsColumn) ? 1 : 0);

                // Create table
                PdfPTable table = new PdfPTable(iColumnCount);
                int[] widths = new int[iColumnCount];
                widths[0] = 75;  // first column (song name) percentage
                for (int x = 1; x < iColumnCount; x++)
                {
                    widths[x] = 10;
                }

                table.SetWidths(widths);

                table.DefaultCell.Padding = 3;
                table.WidthPercentage = 100;  // percentage
                table.TotalWidth = document.Right - document.Left;
                table.DefaultCell.BorderWidth = 0;
                table.DefaultCell.HorizontalAlignment = Element.ALIGN_LEFT;
                table.DefaultCell.VerticalAlignment = Element.ALIGN_CENTER;

                // Add header cells

                AddHeaderCell("Title", ref table); // Song title always prints

                if (slp.bPrintSoloist) AddHeaderCell("Soloist", ref table);
                if (slp.bPrintKey) AddHeaderCell("Key", ref table);
                if (slp.bPrintTempo) AddHeaderCell("Tempo", ref table);
                if (slp.bPrintNote1 && slp.Band.Note1IsColumn) AddHeaderCell(slp.Band.Note1Name, ref table);
                if (slp.bPrintNote2 && slp.Band.Note2IsColumn) AddHeaderCell(slp.Band.Note2Name, ref table);
                if (slp.bPrintNote3 && slp.Band.Note3IsColumn) AddHeaderCell(slp.Band.Note3Name, ref table);
                if (slp.bPrintNote4 && slp.Band.Note4IsColumn) AddHeaderCell(slp.Band.Note4Name, ref table);
                if (slp.bPrintNote5 && slp.Band.Note5IsColumn) AddHeaderCell(slp.Band.Note5Name, ref table);

                // *******************************
                // Iterate each set list item
                // *******************************
                foreach (SetListItem sli in slp.SetListItems)
                {
                    if (sli.SetNumber == current_setnumber)
                    {
                        // This song is in the current set, print it

                        // Only print out Songs
                        if (sli.Note == null)
                        {
                            // Output SONG NAME - set nowrap and fixed height
                            //PdfPCell cell = new PdfPCell(new Phrase(sli.Song.Title, fontSongName));
                            //cell.NoWrap = true;
                            //cell.BorderColor = BaseColor.WHITE;
                            //cell.FixedHeight = 40f;
                            //table.AddCell(cell);

                            Chunk chSongTitle;
                            if (slp.bTruncateSongNames)
                            {
                                // Truncate our song names to column widths
                                int widthSong = (int)table.AbsoluteWidths[0] - 10;
                                chSongTitle = TruncateTextToBox(sli.Song.Title, widthSong, fontSongName);
                            }
                            else
                            {
                                chSongTitle = new Chunk(sli.Song.Title, fontSongName);
                            }

                            table.AddCell(new Phrase(chSongTitle));

                            // Output other columns
                            if (slp.bPrintSoloist) table.AddCell(new Phrase(sli.Song.Soloist, fontColumns));
                            if (slp.bPrintKey) table.AddCell(new Phrase(sli.Song.Key, fontColumns));
                            if (slp.bPrintTempo) table.AddCell(new Phrase(sli.Song.Tempo, fontColumns));
                            if (slp.bPrintNote1 && slp.Band.Note1IsColumn) table.AddCell(new Phrase(sli.Song.Note1, fontColumns));
                            if (slp.bPrintNote2 && slp.Band.Note2IsColumn) table.AddCell(new Phrase(sli.Song.Note2, fontColumns));
                            if (slp.bPrintNote3 && slp.Band.Note3IsColumn) table.AddCell(new Phrase(sli.Song.Note3, fontColumns));
                            if (slp.bPrintNote4 && slp.Band.Note4IsColumn) table.AddCell(new Phrase(sli.Song.Note4, fontColumns));
                            if (slp.bPrintNote5 && slp.Band.Note5IsColumn) table.AddCell(new Phrase(sli.Song.Note5, fontColumns));

                            // Output notes in full-width columns
                            if (slp.bPrintNote1 && !slp.Band.Note1IsColumn) AddNoteCell(sli.Song.Note1, ref table, iColumnCount, fontNotes);
                            if (slp.bPrintNote2 && !slp.Band.Note2IsColumn) AddNoteCell(sli.Song.Note2, ref table, iColumnCount, fontNotes);
                            if (slp.bPrintNote3 && !slp.Band.Note3IsColumn) AddNoteCell(sli.Song.Note3, ref table, iColumnCount, fontNotes);
                            if (slp.bPrintNote4 && !slp.Band.Note4IsColumn) AddNoteCell(sli.Song.Note4, ref table, iColumnCount, fontNotes);
                            if (slp.bPrintNote5 && !slp.Band.Note5IsColumn) AddNoteCell(sli.Song.Note5, ref table, iColumnCount, fontNotes);
                        } // end if item is song
                    } // end if current set 
                } // End loop each set

                // Close out the page
                document.Add(table);
                document.NewPage();

            }// End foreach (Set)

            //close the document
            document.Close();

            //prepare output stream
            Response.ContentType = "application/pdf";
            // Response.AddHeader("content-disposition", "attachment;filename=" + Uri.EscapeDataString(slp.SetList.Name) + ".pdf");
            Response.Buffer = true;
            Response.Clear();
            Response.OutputStream.Write(ms.GetBuffer(), 0, ms.GetBuffer().Length);
            Response.OutputStream.Flush();
            Response.End();

            return new FileStreamResult(Response.OutputStream, "application/pdf");
        }

    }
}
